-- GhostShipM
-- Created by panyl
-- 幽灵船模块

module("GhostShipM", package.seeall);

-- TODO: 幽灵船的位置
local shipPos = 5;

local ships = {};
local shipSkills = {};

local whenEnterNext;
local whenLeaveDungeon;

-- 载入配置表
local _init = false;
local loadCsv = function()
    if _init then return end
    _init = true;

    ships = {};
    shipSkills = {};

    -- 转换下ghost_ship信息表
    local ghost_ship_CSV = get_ghost_ship_CSV();
    for i = 1, #ghost_ship_CSV do
        ships[ghost_ship_CSV[i].class_id] = ghost_ship_CSV[i];

        -- 解析动作帧范围
        parseActionFrameRange(ghost_ship_CSV[i]);
    end

    ghost_ship_CSV = {};

    -- ship_skill表
    local ship_skill_CSV = get_ship_skill_CSV();
    for i = 1, #ship_skill_CSV do
        local row = ship_skill_CSV[i];
        shipSkills[row.skill_id] = row;

        -- 技能整理
        ships[row.class_id]["skills"] = ships[row.class_id]["skills"] or {};

        table.insert(ships[row.class_id]["skills"], row.skill_id);
    end

    ship_skill_CSV = {};
end

function init()
    loadCsv();

    -- 关注进入下一层地牢事件
    EventMgr.removeAll("GhostShipM");
    EventMgr.register("GhostShipM", event.NEXT_FLOOR, whenEnterNext);

    -- 关注层数回退（大地之门等技能）的事件
    EventMgr.register("GhostShipM", event.BACK_FLOOR, function()
        -- 同步一下幽灵船数据
        syncShipData();
    end);

    -- 关注离开地牢事件
    EventMgr.register("GhostShipM", event.LEAVE_DUNGEON, whenLeaveDungeon);

    -- 开始新回合
    CombatM.registerEndRoundCB("ghost_ship_attack", prepareAttack);
end

-- 解析动作的帧范围
function parseActionFrameRange(info)
    -- 待机、攻击、施法
    local fields = { "idle_frame", "attack_frame", "cast_frame",};

    for _, field in pairs(fields) do
        local range = info[field];
        if not string.startWith(range, "[") or not string.endWith(range, "]") then
            error(string.format("字段(%s)配置格式错误!", tostring(field)));
            return info;
        end

        local arr = string.explode(string.sub(range, 2, -2),",");
        if #arr ~= 2 then
            error(string.format("字段(%s)配置格式错误!", tostring(field)));
            return info;
        end

        info[field] = { tonumber(arr[1]), tonumber(arr[2]) };
    end

    return info;
end

-- 进入下一层
whenEnterNext = function()
    syncShipData();
end

function syncShipData()
    local ship = ME.user.ship;
    if ship then
        ME.user.dbase:set("ghost_ship", {
            ["class_id"] = ship.classId,
            ["level"] = ship.level,
            ["hp"] = ship:query("hp"),
            ["skills"] = ship:getAllSkills(),
        });
    end
end

-- 离开地牢，直接丢弃幽灵船数据
whenLeaveDungeon = function()
    ME.user.ship = nil;
    ME.user.dbase:delete("ghost_ship");
    ME.user.dbase:delete("ship_level");
end

-- 开始新一层地牢
function whenStartFloor()

end

-- 检索
function query(classId, path)
    local m = ships[classId];

    if not m or not path then
        return m;
    end

    return m[path];
end

-- 幽灵船所有技能
function getAllSkills()
    return shipSkills;
end

-- 检索幽灵船技能
function querySkill(skillId, path)
    local m = shipSkills[skillId];

    if not m or not path then
        return m;
    end

    return m[path];
end

-- 获取幽灵船数据
function fetchShipData(classId)
    local ghostShip = ME.user.dbase:query("ghost_ship");
    if type(ghostShip) == "table" and #table.keys(ghostShip) > 0 then
        -- 玩家已经雇佣了幽灵船
        return table.add({
            ["hired"] = 1,
            ["class_id"] = classId,
        }, ghostShip);
    end

    -- TODO: 没有被雇佣，野生的幽灵船
    return {};
end

-- 获取幽灵船的属性（被雇佣之后的）
function getShipProp(propId, key)
    local ship = ME.user.ship;

    if not ship or DungeonInstanceM.isInInstance()
        or DungeonBossM.isInBossLayer() then
        -- 副本/boss层没有幽灵船
        return;
    end

    return PropM.combine(ship, propId, key);
end

-- 消耗
function doCost(cost)
    for key, amount in pairs(cost) do
        if type(key) == "number" then
            ItemM.costAmount(ME.user, key, amount);
        elseif type(key) == "string" then
            ME.user:costAttrib(key, amount);
        end
    end
end

-- 创建一个幽灵船
function createShip(classId, info)
    info = info or {};
    if info.skills and info.skills.array then
        info.skills = info.skills:toTable();
    end

    local dbase = table.add({
        ["class_id"] = classId,
        ["level"]  = 1,
        ["skills"] = {},
    }, info);

    local ship = GhostShip.new(classId, dbase);

    -- TODO: 如果幽灵船已经被雇佣了
    if ship:isHired() then
        ME.user.ship = ship;

        -- 记录数据
        ME.user.dbase:set("ghost_ship", {
            ["class_id"] = ship.classId,
            ["level"] = ship.level,
            ["hp"] = ship:query("hp"),
            ["skills"] = ship:getAllSkills(),
        });
    end

    PropM.refresh(ship);

    return ship;
end

-- 计算升级幽灵船技能的消耗
function calcUpgradeSkillCost(skillId)
    return FormulaM.invoke("CALC_UPGRADE_SHIP_SKILL_COST", ME.user, skillId);
end

-- 判断能否升级幽灵船技能
function canUpgradeSkill(skillId)
    local ship = ME.user.ship;

    if not ship then
        -- trace("GhostShipM", "玩家当前没有幽灵船。");
        return false;
    end

    if not ship:ownSkill(skillId) then
        trace("GhostShipM", "幽灵船当前没有技能%d，不能升级。", skillId);
        return false;
    end

    local baseSkill = SkillM.findBaseSkill(skillId);
    local skillLevel = ship:getSkillLevel(skillId);
    if (skillLevel >= querySkill(baseSkill, "max_level")) then
        trace("GhostShipM", "幽灵船的技能%s已经达到最高等级",
            SkillM.query(baseSkill, "name"));
        return false;
    end

    -- 消耗
    local cost = calcUpgradeSkillCost(skillId);
    local ok = true;
    for classId, amount in pairs(cost) do
        if type(classId) == "number" then
            ok = ItemM.getAmount(ME.user, classId) >= amount;
        elseif type(classId) == "string" then
            ok = ME.user.dbase:query(classId, 0) >= amount;
        end

        if true ~= ok then
            return "cost_lack";
        end
    end

    return ok;
end

-- 升级技能
function upgradeSkill(skillId)
    -- 先判断下
    if true ~= canUpgradeSkill(skillId) then
        return false;
    end

    -- 先做消耗
    local cost = calcUpgradeSkillCost(skillId);
    doCost(cost);

    local ship = ME.user.ship;
    ship:upgradeSkill(skillId);

    syncShipData();

    -- 刷新下属性
    PropM.refresh(ship);

    -- 抛出事件
    EventMgr.fire(event.UPGRADE_SHIP_SKILL, { ["skillId"] = skillId, });
    return true;
end

-- 能否升级
function canLevelUp()
    local ship = ME.user.ship;
    if not ship then
        trace("GhostShipM", "玩家没有幽灵船。");
        return false;
    end

    local level = ship:queryAttrib("level");
    if level >= query(ship.classId, "max_level") then
        trace("GhostShipM", "幽灵船已经达到最高等级，不能再升级。");
        return "level_upper";
    end

    -- 没有消耗
    return true;
end

-- 幽灵船升级
function levelUp()
    if true ~= canLevelUp() then
        return false;
    end

    local ship = ME.user.ship;

    -- 幽灵水手+1
    ME.user:addAttrib("ship_level", 1);
    local newLevel = ship:getLevel();

    syncShipData();

    DungeonLogM.addLog(string.format("幽灵水手等级提升，当前%d级。", newLevel));

    -- 抛出事件
    EventMgr.fire(event.SHIP_LEVEL_UP, { ["level"] = newLevel, ["pos"] = ship:getPos(), });

    -- 刷新下属性
    PropM.refresh(ship);

    return true;
end

-- 判断能否使用技能
function canUseSkill(skillId)
    local ship = ME.user.ship;
    if not ship then
        trace("GhostShipM", "没有雇佣幽灵船。");
        return false;
    end

    -- 幽灵船是否拥有此技能
    if not ship:ownSkill(skillId) then
        trace("GhostShipM", "幽灵船没有技能%s。", SkillM.query(skillId, "name"));
        return false;
    end

    -- TODO: 回合数判断（幽灵船技能有CD）
    -- 上次施放技能回合
    if getRemainSkillCD(ship, skillId) > 0 then
        trace("GhostShipM", "幽灵船本回合不能释放技能。");
        return false;
    end

    -- 被禁锢了
    if isImprisonment(ship) then
        trace("GhostShipM", "幽灵船被禁锢了，不能使用技能。");
        return false;
    end

    return true;
end

-- 使用技能
function useSkill(pos, skillId)
    -- 判断
    if true ~= canUseSkill(skillId) then
        return false;
    end

    local ship = ME.user.ship;
    local round = CombatM.getRound();

    ship.skill_round = round + 1;

    -- 施放技能
    local assailantInfo = {
        ["source"] = ship,
        ["skill_id"]   = skillId,
    };
    SkillM.apply(ship, ME.user, skillId, round, { assailantInfo = assailantInfo, });

    -- 等播放完施法动画后再抛出一个回合事件
    -- 一个回合事件
    EventMgr.fire(event.COMBAT_ROUND, -1);

    return true;
end

-- 计算学习技能的消耗
function calcLearnSkillCost(skillId)
    return FormulaM.invoke("CALC_SHIP_LEARN_SKILL_COST", ME.user, skillId);
end

-- 判断能否学习技能
function canLearnSkill(skillId)
    local ship = ME.user.ship;
    if not ship then
        trace("GhostShipM", "没有幽灵船，不能学习幽灵船技能。");
        return false;
    end

    local skills = query(ship.classId, "skills");
    if table.indexOf(skills, skillId) <= 0 then
        trace("GhostShipM", "幽灵船不能学习此技能。");
        return false;
    end

    if ship:ownSkill(skillId) then
        trace("GhostShipM", "幽灵船已经拥有此技能。");
        return false;
    end

    -- 消耗
    local ok = true;
    local cost = calcLearnSkillCost(skillId);
    for classId, amount in pairs(cost) do
        if type(classId) == "number" then
            ok = ItemM.getAmount(ME.user, classId) >= amount;
        elseif type(classId) == "string" then
            ok = ME.user.dbase:query(classId, 0) >= amount;
        end

        if true ~= ok then
            return "cost_lack";
        end
    end

    return ok;
end

-- 学习技能
function learnSkill(skillId)
    if true ~= canLearnSkill(skillId) then
        return false;
    end

    local cost = calcLearnSkillCost(skillId);
    doCost(cost);

    local ship = ME.user.ship;
    ship:learnSkill(skillId);

    syncShipData();

    -- 刷新下属性
    PropM.refresh(ship);

    -- 抛出事件
    EventMgr.fire(event.LEARN_SHIP_SKILL, { ["skillId"] = skillId, });
    return true;
end

-- 能否雇佣
function canEmploy(pos)
    -- 如果玩家已经有幽灵船，就不能再雇佣了
    local ship = ME.user.ship;
    if ship then
        return false;
    end

    local grid = DungeonM.getGridByPos(pos);
    ship = grid.monster;

    if grid.type ~= GRID_TYPE_GHOST_SHIP or
        not ship or not ship:isDead() then
        trace("GhostShipM", "不是幽灵船，或者幽灵船未死亡。");
        return false;
    end

    local ok = true;
    local cost = calcEmployShipCost(ship.classId);
    for classId, amount in pairs(cost) do
        if type(classId) == "number" then
            ok = ItemM.getAmount(ME.user, classId) >= amount;
        elseif type(classId) == "string" then
            ok = ME.user.dbase:query(classId, 0) >= amount;
        end

        if true ~= ok then
            return "cost_lack";
        end
    end

    return ok;
end

-- 计算雇佣幽灵船的消耗
-- @param classId  幽灵船编号
function calcEmployShipCost(classId)
    return FormulaM.invoke("CALC_EMPLOY_SHIP_COST", ME.user, classId);
end

-- 雇佣幽灵船
function employShip(pos)
    if true ~= canEmploy(pos) then
        return false;
    end

    local grid = DungeonM.getGridByPos(pos);
    local ship = grid.monster;
    local dbase = table.add({
        ["hired"] = 1,
    }, fetchShipData(ship.classId));

    -- 先消耗
    local cost = calcEmployShipCost(ship.classId);
    doCost(cost);

    -- 雇佣之后就不是怪物了
    grid.monster = nil;
    grid.ship = ship;

    ship:updateData(dbase);
    ship:revive();

    -- 攻击回合重置
    ship.action = CombatM.getRound();
    ME.user.ship = ship;

    syncShipData();

    DungeonLogM.addLog("雇佣幽灵船。");

    -- 抛出事件
    EventMgr.fire(event.EMPLOY_SHIP, { ["pos"] = pos, });
    return true;
end

-- 幽灵船自动攻击轮次
function autoAttackRound(ship)
    local cd;
    local prop;

    -- 如果已经被雇佣了
    if ship:isHired() then
        cd = query(ship.classId, "cycle");

        prop = PropM.combine(ME.user, "affiliated_cd", "ship");
        cd = PropM.apply(prop, cd);
    else
        cd = query(ship.classId, "cycle2");

        prop = PropM.combine(ship, "add_cd", "slime");
        cd = cd + prop[3];
    end

    prop = PropM.combine(ship, "attack_cd", 1);
    cd = PropM.apply(prop, cd);

    return cd;
end

-- 获取自动攻击轮次倒计时
function getAttackCountDown(ship)
    if ship:isDead() then
        return;
    end

    -- 如果被变形了
    if not ship:isHired() and #PropM.fetchProps(ship, "transfiguration") > 0 then
        return;
    end

    local round = CombatM.getRound();
    local actionRound = ship.action;
    if not actionRound or actionRound <= 0 then
        actionRound = 1;
    end

    local cycle = autoAttackRound(ship);
    if round - actionRound >= cycle then
        return 1;
    end

    return cycle - (round - actionRound) % cycle;
end

-- 获取技能周期
function getSkillCD(ship, skillId)
    local baseSkill = SkillM.findBaseSkill(skillId);
    local cd = querySkill(baseSkill, "skill_cd") or 0;

    return cd;
end

-- 获取技能倒计时
function getRemainSkillCD(ship, skillId)
    -- TODO: 回合数判断（幽灵船技能有CD）
    -- 上次施放技能回合
    local skillRound = ship.skill_round or 1;
    local round = CombatM.getRound();
    local cd = getSkillCD(ship, skillId);
    local last = round - skillRound;

    if last >= cd then
        return 0;
    else
        return cd - last;
    end
end

-- 禁锢
function isImprisonment(ship)
    -- 如果还未被雇佣
    if not ship:isHired() then
        return false;
    end

    -- 遍历一遍所有怪物
    for pos = 1, GRID_SIZE do
        repeat
            local grid = DungeonM.getGridByPos(pos);
            local monster = grid.monster;

            if not grid:isOpened() or not monster or monster:isDead() then
                break;
            end

            -- 某只怪物有禁锢属性
            local prop = PropM.combine(monster, "imprison_ship", 1);
            if prop[3] > 0 then
                return true;
            end

        until true;
    end

    return false;
end

-- 幽灵船自动攻击(前摇)
function prepareAttack()
    -- TODO: 随机选择目标攻击
    local grid = DungeonM.getGridByPos(shipPos);
    local ship;

    if grid.type == GRID_TYPE_GHOST_SHIP then
        ship = grid.ship or grid.monster;
    end

    if not ship or ship:isDead() then
        -- trace("GhostShipM", "没有幽灵船。");
        return false;
    end

    -- 如果被变形了
    if not ship:isHired() and #PropM.fetchProps(ship, "transfiguration") > 0 then
        return false;
    end

    -- 如果被停止了
    if not ship:isHired() and FormulaM.invoke("MONSTER_IS_STOPED", ship) then
        return false;
    end

    -- 当前回合
    local round = CombatM.getRound();
    local actionRound = ship.action;
    if not actionRound or actionRound <= 0 then
        actionRound = 1;
    end

    if (actionRound >= round) then
        trace("GhostShipM", "上次行动回合:%d，当前回合:%d", actionRound, round);
        return false;
    end

    -- 自动攻击回合
    local cycle = autoAttackRound(ship);
    if (round - actionRound) < cycle then
        trace("GhostShipM", "当前回合不能攻击，round = %d，action = %d", round, actionRound);
        return false;
    end

    -- 先记录行动轮次
    ship.action = round;

    -- 被禁锢
    if isImprisonment(ship) then
        trace("GhostShipM", "幽灵船被禁锢了，不能攻击。");

        -- 抛出幽灵船攻击事件
        EventMgr.fire(event.SHIP_ATTACK, { ["pos"] = ship:getPos(), ["result"] = false });
        return false;
    end

    -- 目标
    local target;
    if not ship:isHired() then
        -- 攻击玩家
        target = ME.user;
    else
        -- 攻击怪物，随机一个怪物
        -- 随机选取一个目标
        local grids = {};
        for pos = 1, GRID_SIZE do
            local grid = DungeonM.getGridByPos(pos);
            if grid:isOpened() and grid:isMonster() and not grid.monster:isDead() then
                table.insert(grids, grid);
            end
        end

        if #grids <= 0 then
            trace("GhostShipM", "幽灵船没有攻击目标。");
            EventMgr.fire(event.SHIP_ATTACK_NULL, {["pos"] = ship:getPos(), });
            return false;
        end

        -- 随机一个
        local rand = DungeonM.getRandSeed("ghost_ship_attack") % #grids;
        local grid = grids[rand + 1];
        target = grid.monster;
    end

    local pos;
    if target.type == OBJECT_TYPE_USER then
        pos = 0;
    else
        pos = target:getPos();
    end

    -- 如果目标已经死亡了
    if target:isDead() then
        return false;
    end

    -- 攻击目标
    shipAttack(pos);

    return true, true;
end

function shipAttack(pos)
    local grid = DungeonM.getGridByPos(shipPos);
    local ship;

    if grid.type == GRID_TYPE_GHOST_SHIP then
        ship = grid.ship or grid.monster;
    end

    if not ship or ship:isDead() then
        -- trace("GhostShipM", "没有幽灵船。");
        return false;
    end

    local target;
    if pos and pos > 0 then
        grid = DungeonM.getGridByPos(pos);
        target = grid.monster;
    else
        target = ME.user;
    end

    if SkillM.canPhysic(ship, target) then
        -- 如果满足物理攻击条件，先抛出攻击前摇事件
        EventMgr.fire(event.SHIP_PRE_ATTACK, { ["pos"] = pos, ["ship_pos"] = ship:getPos(), });
    end

    SkillM.hit(ship, target, 0);

    -- 抛出幽灵船攻击事件
    EventMgr.fire(event.SHIP_ATTACK, { ["pos"] = ship:getPos(), ["result"] = true });

    DungeonLogM.addLog(string.format("幽灵船攻击%s。", target:getName()));
end

-- 获取幽灵船主动技能
function getInitiativeSkill()
    local ship = ME.user.ship;
    local skills = query(ship.classId, "skills");

    for _, skillId in pairs(skills) do
        if SkillM.query(skillId, "type") == USER_SKILL then
            -- 如果已经学习过，则返回对应等级的技能
            if ship:ownSkill(skillId) then
                local level = ship:getSkillLevel(skillId);
                return SkillM.findSkill(skillId, level);
            end

            -- 未学习过，默认返回初级技能
            return skillId;
        end
    end

    return nil;
end

-- 幽灵船坞是否需要保底
function checkGiveDragonCarronade()
    -- 正常可获取到的幽灵船数据
    if ME.user.ship then
        if ME.user.ship:ownSkill(6201) then
            -- 有该类技能
            return false;
        end
    else
        -- 重启客户端进入时尚未创建幽灵船，从玩家身上取
        local info = ME.user.dbase:query("ghost_ship");
        if info and info.skills then
            if info.skills.array then
                info.skills = info.skills:toTable();
            end

            local base = SkillM.findBaseSkill(6201);
            for _, id in pairs(info.skills) do
                if SkillM.findBaseSkill(id) == base then
                    -- 有该类技能
                    return false;
                end
            end
        end
    end
    return true;
end

-- 幽灵船坞物品对应基础技
function goodBaseSkillInfo()
    return { [702] = 6201, [703] = 6211, [704] = 6221, [705] = 6231};
end

-- 幽灵船坞物品对应最高技
function goodMaxSkillInfo()
    return { [702] = 6205, [703] = 6215, [704] = 6225, [705] = 6235};
end

-- 幽灵船某物品对应技能是否满级
function checkSkillLevelMax(goodId)

    local maxSkill  = goodMaxSkillInfo()[goodId];

    -- 正常可获取到的幽灵船数据
    if ME.user.ship then
        local skills = ME.user.ship:getSkills() or {};
        if table.indexOf(skills, maxSkill) ~= -1 then
            -- 满级
            return true;
        end
    else
        -- 重启客户端进入时尚未创建幽灵船，从玩家身上取
        local info = ME.user.dbase:query("ghost_ship");
        if info and info.skills then
            if info.skills.array then
                info.skills = info.skills:toTable();
            end

            for _, id in pairs(info.skills) do
                if table.indexOf(info.skills, maxSkill) ~= -1 then
                    -- 满级
                    return true;
                end
            end
        end
    end

    return false;
end

-- 幽灵船能否升级
function checkCanLevelUp()
    if ME.user.ship then
        -- 有幽灵船数据
        if canLevelUp() ~= true then
            return false;
        end
    else
        local info = ME.user.dbase:query("ghost_ship");

        if info then
            local shipLevel = (ME.user.dbase:query("ship_level") or 0) + (info["level"] or 1);

            -- 有幽灵船数据
            if not info.class_id then
                -- 幽灵船长初始携带，没有class_id数据
                return true;
            end

            if shipLevel >= query(info.class_id, "max_level") then
                return false;
            end
        end
    end

    return true;
end
